1533 lines
54 KiB
Vue
1533 lines
54 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<div class="flex flex-col lg:flex-row gap-6">
|
||
<!-- 左侧配置面板 -->
|
||
<div class="flex-1 lg:max-w-2xl space-y-4">
|
||
<!-- 基础参数 -->
|
||
<div class="card p-6">
|
||
<h3 class="text-lg font-medium text-primary mb-4">
|
||
<FontAwesomeIcon :icon="['fas', 'cogs']" class="mr-2" />
|
||
基础参数
|
||
</h3>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<!-- 印章样式 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">印章样式</label>
|
||
<select v-model="sealType" class="select-field w-full">
|
||
<option value="company_round">公司圆章</option>
|
||
<option value="company_oval">公司椭圆章</option>
|
||
<option value="personal_square">个人方章</option>
|
||
<option value="personal_round">个人圆章</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 印章大小 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">印章大小 ({{ sealSize }}px)</label>
|
||
<input
|
||
v-model.number="sealSize"
|
||
type="range"
|
||
min="200"
|
||
max="600"
|
||
step="20"
|
||
class="slider w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 个人方章高度调整 -->
|
||
<div v-if="sealType === 'personal_square'">
|
||
<label class="block text-sm font-medium text-secondary mb-2">印章高度比例 ({{ personalHeight }})</label>
|
||
<input
|
||
v-model.number="personalHeight"
|
||
type="range"
|
||
min="0.5"
|
||
max="2.0"
|
||
step="0.1"
|
||
class="slider w-full"
|
||
/>
|
||
<div class="flex justify-between text-xs text-tertiary mt-1">
|
||
<span>扁平</span>
|
||
<span>正方</span>
|
||
<span>高长</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 印章颜色 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">印章颜色</label>
|
||
<div class="flex items-center gap-2">
|
||
<input
|
||
v-model="sealColor"
|
||
type="color"
|
||
class="w-10 h-10 rounded border border-primary/20"
|
||
/>
|
||
<input
|
||
v-model="sealColor"
|
||
type="text"
|
||
class="input-field flex-1"
|
||
placeholder="#FF0000"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 背景颜色 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">背景颜色</label>
|
||
<div class="flex items-center gap-2">
|
||
<input
|
||
v-model="backgroundColor"
|
||
type="color"
|
||
class="w-10 h-10 rounded border border-primary/20"
|
||
/>
|
||
<input
|
||
v-model="backgroundColor"
|
||
type="text"
|
||
class="input-field flex-1"
|
||
placeholder="#FFFFFF"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- 主文字设置 -->
|
||
<div class="card p-6">
|
||
<h3 class="text-lg font-medium text-primary mb-4">
|
||
<FontAwesomeIcon :icon="['fas', 'font']" class="mr-2" />
|
||
主文字设置
|
||
</h3>
|
||
|
||
<div class="space-y-4">
|
||
<!-- 主文字内容 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">文字内容</label>
|
||
<input
|
||
v-model="mainText"
|
||
type="text"
|
||
class="input-field w-full"
|
||
:placeholder="sealType.startsWith('company') ? '公司名称' : '姓名'"
|
||
maxlength="20"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 主文字大小 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">字体大小 ({{ mainFontSize }}px)</label>
|
||
<input
|
||
v-model.number="mainFontSize"
|
||
type="range"
|
||
min="16"
|
||
max="60"
|
||
step="2"
|
||
class="slider w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 主文字字体样式 -->
|
||
<div class="space-y-3">
|
||
<!-- 字体选择 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">字体</label>
|
||
<select v-model="mainFontFamily" class="select-field w-full">
|
||
<option value="SimSun">宋体 (SimSun)</option>
|
||
<option value="SimHei">黑体 (SimHei)</option>
|
||
<option value="KaiTi">楷体 (KaiTi)</option>
|
||
<option value="FangSong">仿宋 (FangSong)</option>
|
||
<option value="Microsoft YaHei">微软雅黑 (Microsoft YaHei)</option>
|
||
<option value="Arial">Arial</option>
|
||
<option value="Times New Roman">Times New Roman</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 字体样式 -->
|
||
<div class="flex gap-4">
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="mainFontWeight"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">加粗</span>
|
||
</label>
|
||
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="mainFontItalic"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">斜体</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主文字布局(仅公司章) -->
|
||
<div v-if="sealType.startsWith('company')" class="space-y-4">
|
||
<!-- 文字间距 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">文字间距 ({{ textSpacing }})</label>
|
||
<input
|
||
v-model.number="textSpacing"
|
||
type="range"
|
||
min="0.5"
|
||
max="1.5"
|
||
step="0.05"
|
||
class="slider w-full"
|
||
/>
|
||
<div class="flex justify-between text-xs text-tertiary mt-1">
|
||
<span>紧密</span>
|
||
<span>适中</span>
|
||
<span>宽松</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主文字位置 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">文字位置 ({{ textMargin > 0 ? '+' : '' }}{{ textMargin }}px)</label>
|
||
<input
|
||
v-model.number="textMargin"
|
||
type="range"
|
||
min="-30"
|
||
max="30"
|
||
step="2"
|
||
class="slider w-full"
|
||
/>
|
||
<div class="flex justify-between text-xs text-tertiary mt-1">
|
||
<span>向外</span>
|
||
<span>标准</span>
|
||
<span>向内</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中心文字设置(仅公司章) -->
|
||
<div v-if="sealType.startsWith('company')" class="card p-6">
|
||
<h3 class="text-lg font-medium text-primary mb-4">
|
||
<FontAwesomeIcon :icon="['fas', 'dot-circle']" class="mr-2" />
|
||
中心文字设置
|
||
</h3>
|
||
|
||
<div class="space-y-4">
|
||
<!-- 中心文字内容 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">文字内容</label>
|
||
<input
|
||
v-model="centerText"
|
||
type="text"
|
||
class="input-field w-full"
|
||
placeholder="印"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 中心文字大小 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">字体大小 ({{ centerFontSize }}px)</label>
|
||
<input
|
||
v-model.number="centerFontSize"
|
||
type="range"
|
||
min="20"
|
||
max="80"
|
||
step="2"
|
||
class="slider w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 中心文字字体样式 -->
|
||
<div class="space-y-3">
|
||
<!-- 字体选择 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">字体</label>
|
||
<select v-model="centerFontFamily" class="select-field w-full">
|
||
<option value="SimSun">宋体 (SimSun)</option>
|
||
<option value="SimHei">黑体 (SimHei)</option>
|
||
<option value="KaiTi">楷体 (KaiTi)</option>
|
||
<option value="FangSong">仿宋 (FangSong)</option>
|
||
<option value="Microsoft YaHei">微软雅黑 (Microsoft YaHei)</option>
|
||
<option value="Arial">Arial</option>
|
||
<option value="Times New Roman">Times New Roman</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 字体样式 -->
|
||
<div class="flex gap-4">
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="centerFontWeight"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">加粗</span>
|
||
</label>
|
||
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="centerFontItalic"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">斜体</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- 中心图形设置 -->
|
||
<div class="mt-4 space-y-3">
|
||
<!-- 图形选择 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">中心图形</label>
|
||
<select v-model="centerShape" class="select-field w-full">
|
||
<option value="">无图形</option>
|
||
<option value="star">五角星</option>
|
||
<option value="circle">圆形</option>
|
||
<option value="diamond">菱形</option>
|
||
<option value="hexagram">六角星</option>
|
||
<option value="triangle">三角形</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 图形大小 -->
|
||
<div v-if="centerShape">
|
||
<label class="block text-sm font-medium text-secondary mb-2">图形大小 ({{ centerShapeSize }}px)</label>
|
||
<input
|
||
v-model.number="centerShapeSize"
|
||
type="range"
|
||
min="8"
|
||
max="30"
|
||
step="2"
|
||
class="slider w-full"
|
||
/>
|
||
<div class="flex justify-between text-xs text-tertiary mt-1">
|
||
<span>小</span>
|
||
<span>中</span>
|
||
<span>大</span>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="text-xs text-tertiary">
|
||
智能居中:无文字时图形居中,无图形时文字居中
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 抬头文字设置(仅公司章) -->
|
||
<div v-if="sealType.startsWith('company')" class="card p-6">
|
||
<h3 class="text-lg font-medium text-primary mb-4">
|
||
<FontAwesomeIcon :icon="['fas', 'chevron-up']" class="mr-2" />
|
||
抬头文字设置
|
||
</h3>
|
||
|
||
<div class="space-y-4">
|
||
<!-- 抬头文字内容 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">文字内容</label>
|
||
<input
|
||
v-model="headerText"
|
||
type="text"
|
||
class="input-field w-full"
|
||
placeholder="例:总公司、分公司"
|
||
maxlength="10"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 抬头文字大小 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">字体大小 ({{ headerFontSize }}px)</label>
|
||
<input
|
||
v-model.number="headerFontSize"
|
||
type="range"
|
||
min="12"
|
||
max="40"
|
||
step="2"
|
||
class="slider w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 抬头文字字体样式 -->
|
||
<div class="space-y-3">
|
||
<!-- 字体选择 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">字体</label>
|
||
<select v-model="headerFontFamily" class="select-field w-full">
|
||
<option value="SimSun">宋体 (SimSun)</option>
|
||
<option value="SimHei">黑体 (SimHei)</option>
|
||
<option value="KaiTi">楷体 (KaiTi)</option>
|
||
<option value="FangSong">仿宋 (FangSong)</option>
|
||
<option value="Microsoft YaHei">微软雅黑 (Microsoft YaHei)</option>
|
||
<option value="Arial">Arial</option>
|
||
<option value="Times New Roman">Times New Roman</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 字体样式 -->
|
||
<div class="flex gap-4">
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="headerFontWeight"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">加粗</span>
|
||
</label>
|
||
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="headerFontItalic"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">斜体</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 副文字设置(仅公司章) -->
|
||
<div v-if="sealType.startsWith('company')" class="card p-6">
|
||
<h3 class="text-lg font-medium text-primary mb-4">
|
||
<FontAwesomeIcon :icon="['fas', 'chevron-down']" class="mr-2" />
|
||
副文字设置
|
||
</h3>
|
||
|
||
<div class="space-y-4">
|
||
<!-- 副文字内容 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">文字内容</label>
|
||
<input
|
||
v-model="subText"
|
||
type="text"
|
||
class="input-field w-full"
|
||
placeholder="例:有限公司、股份有限公司"
|
||
maxlength="10"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 副文字大小 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">字体大小 ({{ subFontSize }}px)</label>
|
||
<input
|
||
v-model.number="subFontSize"
|
||
type="range"
|
||
min="12"
|
||
max="40"
|
||
step="2"
|
||
class="slider w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 副文字字体样式 -->
|
||
<div class="space-y-3">
|
||
<!-- 显示模式 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">显示模式</label>
|
||
<div class="flex gap-4">
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="subTextCurved"
|
||
type="radio"
|
||
:value="false"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">平直显示</span>
|
||
</label>
|
||
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="subTextCurved"
|
||
type="radio"
|
||
:value="true"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">弧形显示</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 副文字间距(弧形模式) -->
|
||
<div v-if="subTextCurved">
|
||
<label class="block text-sm font-medium text-secondary mb-2">文字间距 ({{ subTextSpacing }})</label>
|
||
<input
|
||
v-model.number="subTextSpacing"
|
||
type="range"
|
||
min="0.5"
|
||
max="1.5"
|
||
step="0.05"
|
||
class="slider w-full"
|
||
/>
|
||
<div class="flex justify-between text-xs text-tertiary mt-1">
|
||
<span>紧密</span>
|
||
<span>适中</span>
|
||
<span>宽松</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 副文字位置 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">文字位置 ({{ subTextMargin > 0 ? '+' : '' }}{{ subTextMargin }}px)</label>
|
||
<input
|
||
v-model.number="subTextMargin"
|
||
type="range"
|
||
min="-30"
|
||
max="30"
|
||
step="2"
|
||
class="slider w-full"
|
||
/>
|
||
<div class="flex justify-between text-xs text-tertiary mt-1">
|
||
<span>向外</span>
|
||
<span>标准</span>
|
||
<span>向内</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 字体选择 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">字体</label>
|
||
<select v-model="subFontFamily" class="select-field w-full">
|
||
<option value="SimSun">宋体 (SimSun)</option>
|
||
<option value="SimHei">黑体 (SimHei)</option>
|
||
<option value="KaiTi">楷体 (KaiTi)</option>
|
||
<option value="FangSong">仿宋 (FangSong)</option>
|
||
<option value="Microsoft YaHei">微软雅黑 (Microsoft YaHei)</option>
|
||
<option value="Arial">Arial</option>
|
||
<option value="Times New Roman">Times New Roman</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 字体样式 -->
|
||
<div class="flex gap-4">
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="subFontWeight"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">加粗</span>
|
||
</label>
|
||
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="subFontItalic"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">斜体</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 边框设置 -->
|
||
<div class="card p-6">
|
||
<h3 class="text-lg font-medium text-primary mb-4">
|
||
<FontAwesomeIcon :icon="['fas', 'square']" class="mr-2" />
|
||
边框设置
|
||
</h3>
|
||
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<!-- 外边线宽度 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">外边线宽度 ({{ outerBorderWidth }}px)</label>
|
||
<input
|
||
v-model.number="outerBorderWidth"
|
||
type="range"
|
||
min="1"
|
||
max="10"
|
||
step="1"
|
||
class="slider w-full"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 内边线宽度 -->
|
||
<div v-if="sealType.startsWith('company')" class="col-span-2">
|
||
<label class="block text-sm font-medium text-secondary mb-2">内边线宽度 ({{ innerBorderWidth }}px)</label>
|
||
<input
|
||
v-model.number="innerBorderWidth"
|
||
type="range"
|
||
min="0"
|
||
max="5"
|
||
step="1"
|
||
class="slider w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="card p-6">
|
||
<div class="space-y-4">
|
||
<!-- 导出选项 -->
|
||
<div class="space-y-3">
|
||
<label class="flex items-center">
|
||
<input
|
||
v-model="exportTransparent"
|
||
type="checkbox"
|
||
class="mr-2"
|
||
/>
|
||
<span class="text-sm text-secondary">导出透明背景PNG</span>
|
||
</label>
|
||
|
||
<!-- 破损样式 -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">破损样式</label>
|
||
<select v-model="damageStyle" class="select-field w-full">
|
||
<option value="none">无破损</option>
|
||
<option value="random">随机磨损</option>
|
||
<option value="cracks">裂纹破损</option>
|
||
<option value="edge">边缘磨损</option>
|
||
<option value="chunks">块状缺失</option>
|
||
<option value="stains">污渍斑点</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 破损程度 -->
|
||
<div v-if="damageStyle !== 'none'">
|
||
<label class="block text-sm font-medium text-secondary mb-2">破损程度 ({{ wearLevel }}%)</label>
|
||
<input
|
||
v-model.number="wearLevel"
|
||
type="range"
|
||
min="0"
|
||
max="50"
|
||
step="5"
|
||
class="slider w-full"
|
||
/>
|
||
<div class="flex justify-between text-xs text-tertiary mt-1">
|
||
<span>轻微</span>
|
||
<span>中等</span>
|
||
<span>严重</span>
|
||
</div>
|
||
<p class="text-xs text-tertiary mt-1">
|
||
{{ getDamageDescription(damageStyle) }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex gap-3">
|
||
<button
|
||
@click="downloadSeal"
|
||
class="btn-primary flex-1"
|
||
:disabled="!mainText.trim()"
|
||
>
|
||
<FontAwesomeIcon :icon="['fas', 'download']" class="mr-2" />
|
||
下载印章
|
||
</button>
|
||
|
||
<button
|
||
@click="clearAll"
|
||
class="btn-secondary"
|
||
>
|
||
<FontAwesomeIcon :icon="['fas', 'trash']" class="mr-2" />
|
||
清空
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧预览区域 -->
|
||
<div class="lg:fixed lg:right-4 lg:w-96 lg:max-h-[calc(100vh-2rem)]">
|
||
<div class="card p-6">
|
||
<h3 class="text-lg font-medium text-primary mb-4">
|
||
<FontAwesomeIcon :icon="['fas', 'eye']" class="mr-2" />
|
||
印章预览 - 实时显示
|
||
</h3>
|
||
|
||
<div class="flex justify-center items-center min-h-[400px] bg-block rounded-lg p-4">
|
||
<canvas
|
||
ref="sealCanvas"
|
||
:width="canvasWidth"
|
||
:height="canvasHeight"
|
||
class="border border-primary/20 rounded-lg shadow-lg"
|
||
style="max-width: 100%; height: auto;"
|
||
/>
|
||
</div>
|
||
|
||
<div class="mt-4 text-center">
|
||
<p class="text-sm text-tertiary">
|
||
<FontAwesomeIcon :icon="['fas', 'info-circle']" class="mr-1" />
|
||
左侧调整参数,右侧实时预览
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 使用说明 -->
|
||
<div class="card p-6">
|
||
<h3 class="text-md font-medium text-primary mb-4">
|
||
<FontAwesomeIcon :icon="['fas', 'info-circle']" class="mr-2" />
|
||
使用说明
|
||
</h3>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div>
|
||
<h4 class="font-medium text-secondary mb-2">公司印章</h4>
|
||
<ul class="text-sm text-tertiary space-y-1">
|
||
<li>• 支持圆形和椭圆形样式</li>
|
||
<li>• 可设置主文字(公司名称)</li>
|
||
<li>• 可添加中心文字、抬头文字、副文字</li>
|
||
<li>• 支持五角星装饰</li>
|
||
<li>• 可调节内外边框宽度</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 class="font-medium text-secondary mb-2">个人印章</h4>
|
||
<ul class="text-sm text-tertiary space-y-1">
|
||
<li>• 支持方形和圆形样式</li>
|
||
<li>• 适用于个人姓名印章</li>
|
||
<li>• 支持2-4个汉字</li>
|
||
<li>• 简洁美观的设计</li>
|
||
<li>• 可自定义颜色和大小</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-4 p-4 bg-block rounded-lg">
|
||
<h4 class="font-medium text-secondary mb-2">
|
||
<FontAwesomeIcon :icon="['fas', 'exclamation-triangle']" class="mr-2 text-yellow-500" />
|
||
重要提示
|
||
</h4>
|
||
<p class="text-sm text-tertiary">
|
||
本工具生成的印章仅供学习和测试使用,请勿用于非法用途。正式的公司印章需要到相关部门进行备案注册。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||
|
||
// 响应式状态
|
||
const sealType = ref('company_round')
|
||
const sealSize = ref(300)
|
||
|
||
// 计算画布尺寸
|
||
const canvasWidth = computed(() => {
|
||
if (sealType.value === 'company_oval') {
|
||
return sealSize.value
|
||
}
|
||
return sealSize.value
|
||
})
|
||
|
||
const canvasHeight = computed(() => {
|
||
if (sealType.value === 'company_oval') {
|
||
return Math.round(sealSize.value * 0.8) // 椭圆形高度为宽度的80%
|
||
} else if (sealType.value === 'personal_square') {
|
||
return Math.round(sealSize.value * personalHeight.value) // 个人方章支持高度调整
|
||
}
|
||
return sealSize.value
|
||
})
|
||
const sealColor = ref('#FF0000')
|
||
const backgroundColor = ref('#FFFFFF')
|
||
const mainText = ref('子归云科技')
|
||
const centerText = ref('印')
|
||
const headerText = ref('')
|
||
const subText = ref('')
|
||
const mainFontSize = ref(24)
|
||
const centerFontSize = ref(40)
|
||
const headerFontSize = ref(18)
|
||
const subFontSize = ref(18)
|
||
const outerBorderWidth = ref(3)
|
||
const innerBorderWidth = ref(1)
|
||
const showStar = ref(true) // 保留用于兼容性
|
||
const centerShape = ref('star') // 中心图形类型
|
||
const centerShapeSize = ref(15) // 中心图形大小
|
||
const textSpacing = ref(1.0)
|
||
const textMargin = ref(0)
|
||
const personalHeight = ref(1.0) // 个人印章高度比例
|
||
|
||
// 主文字样式
|
||
const mainFontWeight = ref(false)
|
||
const mainFontItalic = ref(false)
|
||
const mainFontFamily = ref('SimSun')
|
||
|
||
// 中心文字样式
|
||
const centerFontWeight = ref(false)
|
||
const centerFontItalic = ref(false)
|
||
const centerFontFamily = ref('SimSun')
|
||
|
||
// 抬头文字样式
|
||
const headerFontWeight = ref(false)
|
||
const headerFontItalic = ref(false)
|
||
const headerFontFamily = ref('SimSun')
|
||
|
||
// 副文字样式
|
||
const subFontWeight = ref(false)
|
||
const subFontItalic = ref(false)
|
||
const subFontFamily = ref('SimSun')
|
||
const subTextCurved = ref(false) // 副文字是否弧形显示
|
||
const subTextSpacing = ref(1.0) // 副文字间距
|
||
const subTextMargin = ref(0) // 副文字位置调整
|
||
const exportTransparent = ref(true) // 是否导出透明背景
|
||
const wearLevel = ref(0) // 印章旧度(0-50%)
|
||
const damageStyle = ref('none') // 破损样式
|
||
|
||
const isPreviewFixed = ref(false)
|
||
|
||
const sealCanvas = ref<HTMLCanvasElement>()
|
||
|
||
// 构建字体样式字符串
|
||
const buildFontStyle = (fontSize: number, weight: boolean, italic: boolean, family: string) => {
|
||
const fontWeight = weight ? 'bold' : 'normal'
|
||
const fontStyle = italic ? 'italic' : 'normal'
|
||
return `${fontStyle} ${fontWeight} ${fontSize}px ${family}, serif`
|
||
}
|
||
|
||
// 获取破损样式描述
|
||
const getDamageDescription = (style: string) => {
|
||
const descriptions = {
|
||
none: '无破损效果',
|
||
random: '随机像素磨损,模拟自然使用痕迹',
|
||
cracks: '生成裂纹线条,模拟印章开裂',
|
||
edge: '边缘优先磨损,模拟边缘磨损',
|
||
chunks: '块状区域缺失,模拟大块破损',
|
||
stains: '污渍斑点效果,模拟墨水污染'
|
||
}
|
||
return descriptions[style as keyof typeof descriptions] || '未知样式'
|
||
}
|
||
|
||
// 绘制印章
|
||
const drawSeal = async (transparent = false) => {
|
||
await nextTick()
|
||
const canvas = sealCanvas.value
|
||
if (!canvas) return
|
||
|
||
const ctx = canvas.getContext('2d')
|
||
if (!ctx) return
|
||
|
||
// 清空画布
|
||
ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
|
||
|
||
// 如果没有主文字,显示提示(仅在预览模式,非透明导出模式)
|
||
if (!mainText.value.trim() && !transparent) {
|
||
// 先绘制背景
|
||
ctx.fillStyle = backgroundColor.value
|
||
ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
|
||
|
||
ctx.fillStyle = '#999999'
|
||
ctx.font = buildFontStyle(16, false, false, 'SimSun')
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'middle'
|
||
ctx.fillText('请输入印章文字', canvasWidth.value / 2, canvasHeight.value / 2)
|
||
return
|
||
}
|
||
|
||
// 如果有破损效果且是预览模式,需要特殊处理
|
||
const hasDamage = damageStyle.value !== 'none' && wearLevel.value > 0
|
||
|
||
if (!transparent && hasDamage) {
|
||
// 预览模式且有破损效果:先在临时画布绘制完整印章,再复制到主画布
|
||
const tempCanvas = document.createElement('canvas')
|
||
tempCanvas.width = canvasWidth.value
|
||
tempCanvas.height = canvasHeight.value
|
||
const tempCtx = tempCanvas.getContext('2d')
|
||
if (!tempCtx) return
|
||
|
||
// 在临时画布绘制印章
|
||
tempCtx.strokeStyle = sealColor.value
|
||
tempCtx.fillStyle = sealColor.value
|
||
|
||
if (sealType.value.startsWith('company')) {
|
||
drawCompanySeal(tempCtx)
|
||
} else {
|
||
drawPersonalSeal(tempCtx)
|
||
}
|
||
|
||
// 应用破损效果到临时画布
|
||
applyDamageEffect(tempCtx)
|
||
|
||
// 绘制背景到主画布
|
||
ctx.fillStyle = backgroundColor.value
|
||
ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
|
||
|
||
// 将处理好的印章复制到主画布
|
||
ctx.drawImage(tempCanvas, 0, 0)
|
||
} else {
|
||
// 正常绘制流程(导出模式或无破损效果)
|
||
if (!transparent) {
|
||
ctx.fillStyle = backgroundColor.value
|
||
ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
|
||
}
|
||
|
||
// 根据印章类型绘制
|
||
if (sealType.value.startsWith('company')) {
|
||
drawCompanySeal(ctx)
|
||
} else {
|
||
drawPersonalSeal(ctx)
|
||
}
|
||
|
||
// 应用破损效果
|
||
if (hasDamage) {
|
||
applyDamageEffect(ctx)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 绘制公司印章
|
||
const drawCompanySeal = (ctx: CanvasRenderingContext2D) => {
|
||
const centerX = canvasWidth.value / 2
|
||
const centerY = canvasHeight.value / 2
|
||
const radius = Math.min(canvasWidth.value, canvasHeight.value) * 0.4
|
||
|
||
ctx.strokeStyle = sealColor.value
|
||
ctx.fillStyle = sealColor.value
|
||
|
||
// 绘制外边框
|
||
ctx.lineWidth = outerBorderWidth.value
|
||
ctx.beginPath()
|
||
if (sealType.value === 'company_oval') {
|
||
// 椭圆形
|
||
ctx.ellipse(centerX, centerY, radius, radius * 0.8, 0, 0, 2 * Math.PI)
|
||
} else {
|
||
// 圆形
|
||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI)
|
||
}
|
||
ctx.stroke()
|
||
|
||
// 绘制内边框
|
||
if (innerBorderWidth.value > 0) {
|
||
ctx.lineWidth = innerBorderWidth.value
|
||
ctx.beginPath()
|
||
if (sealType.value === 'company_oval') {
|
||
ctx.ellipse(centerX, centerY, radius - 15, (radius - 15) * 0.8, 0, 0, 2 * Math.PI)
|
||
} else {
|
||
ctx.arc(centerX, centerY, radius - 15, 0, 2 * Math.PI)
|
||
}
|
||
ctx.stroke()
|
||
}
|
||
|
||
// 绘制主文字(圆弧排列)
|
||
const textRadius = radius - 25 - textMargin.value // 应用外边距调整
|
||
drawCurvedText(ctx, mainText.value, centerX, centerY, textRadius, 0.8)
|
||
|
||
// 绘制抬头文字
|
||
if (headerText.value.trim()) {
|
||
ctx.font = buildFontStyle(headerFontSize.value, headerFontWeight.value, headerFontItalic.value, headerFontFamily.value)
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'middle'
|
||
ctx.fillText(headerText.value, centerX, centerY - radius * 0.3)
|
||
}
|
||
|
||
// 绘制副文字
|
||
if (subText.value.trim()) {
|
||
if (subTextCurved.value) {
|
||
// 弧形显示
|
||
drawSubCurvedText(ctx, subText.value, centerX, centerY, radius - 25 + subTextMargin.value)
|
||
} else {
|
||
// 平直显示
|
||
ctx.font = buildFontStyle(subFontSize.value, subFontWeight.value, subFontItalic.value, subFontFamily.value)
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'middle'
|
||
ctx.fillText(subText.value, centerX, centerY + radius * 0.7 + subTextMargin.value)
|
||
}
|
||
}
|
||
|
||
// 智能绘制中心图形和文字
|
||
const hasCenterText = centerText.value.trim()
|
||
const hasShape = centerShape.value !== ''
|
||
|
||
if (hasShape && hasCenterText) {
|
||
// 同时显示图形和文字:自适应布局
|
||
const shapeSize = centerShapeSize.value
|
||
const textSize = centerFontSize.value
|
||
|
||
// 计算自适应间距:基于图形和文字的大小
|
||
const baseSpacing = 5 // 基础间距
|
||
const shapeSpacing = shapeSize * 0.8 // 图形占用空间
|
||
const textSpacing = textSize * 0.6 // 文字占用空间
|
||
|
||
// 图形位置:向上偏移(大小越大偏移越多)
|
||
const shapeY = centerY - baseSpacing - textSpacing
|
||
|
||
// 文字位置:向下偏移(大小越大偏移越多)
|
||
const textY = centerY + baseSpacing + shapeSpacing
|
||
|
||
// 绘制图形
|
||
drawCenterShape(ctx, centerX, shapeY, shapeSize, centerShape.value)
|
||
|
||
// 绘制文字
|
||
ctx.font = buildFontStyle(centerFontSize.value, centerFontWeight.value, centerFontItalic.value, centerFontFamily.value)
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'middle'
|
||
ctx.fillText(centerText.value, centerX, textY)
|
||
|
||
} else if (hasShape && !hasCenterText) {
|
||
// 只显示图形:居中显示
|
||
drawCenterShape(ctx, centerX, centerY, centerShapeSize.value, centerShape.value)
|
||
} else if (!hasShape && hasCenterText) {
|
||
// 只显示文字:居中显示
|
||
ctx.font = buildFontStyle(centerFontSize.value, centerFontWeight.value, centerFontItalic.value, centerFontFamily.value)
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'middle'
|
||
ctx.fillText(centerText.value, centerX, centerY)
|
||
}
|
||
// 如果都不显示,则什么都不绘制
|
||
}
|
||
|
||
// 绘制个人印章
|
||
const drawPersonalSeal = (ctx: CanvasRenderingContext2D) => {
|
||
const centerX = canvasWidth.value / 2
|
||
const centerY = canvasHeight.value / 2
|
||
|
||
ctx.strokeStyle = sealColor.value
|
||
ctx.fillStyle = sealColor.value
|
||
ctx.lineWidth = outerBorderWidth.value
|
||
|
||
// 绘制边框
|
||
ctx.beginPath()
|
||
if (sealType.value === 'personal_round') {
|
||
// 圆形
|
||
const size = Math.min(canvasWidth.value, canvasHeight.value) * 0.8
|
||
ctx.arc(centerX, centerY, size / 2, 0, 2 * Math.PI)
|
||
} else {
|
||
// 方形/矩形
|
||
const width = canvasWidth.value * 0.8
|
||
const height = canvasHeight.value * 0.8
|
||
ctx.rect(centerX - width / 2, centerY - height / 2, width, height)
|
||
}
|
||
ctx.stroke()
|
||
|
||
// 绘制文字
|
||
const text = mainText.value.trim()
|
||
const textLength = text.length
|
||
|
||
ctx.font = buildFontStyle(mainFontSize.value, mainFontWeight.value, mainFontItalic.value, mainFontFamily.value)
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'middle'
|
||
|
||
if (textLength === 2) {
|
||
// 两字:上下排列
|
||
ctx.fillText(text[0], centerX, centerY - mainFontSize.value * 0.6)
|
||
ctx.fillText(text[1], centerX, centerY + mainFontSize.value * 0.6)
|
||
} else if (textLength === 3) {
|
||
// 三字:一字在上,两字在下
|
||
ctx.fillText(text[0], centerX, centerY - mainFontSize.value * 0.8)
|
||
ctx.fillText(text[1], centerX - mainFontSize.value * 0.7, centerY + mainFontSize.value * 0.4)
|
||
ctx.fillText(text[2], centerX + mainFontSize.value * 0.7, centerY + mainFontSize.value * 0.4)
|
||
} else if (textLength === 4) {
|
||
// 四字:田字格排列
|
||
ctx.fillText(text[0], centerX - mainFontSize.value * 0.6, centerY - mainFontSize.value * 0.6)
|
||
ctx.fillText(text[1], centerX + mainFontSize.value * 0.6, centerY - mainFontSize.value * 0.6)
|
||
ctx.fillText(text[2], centerX - mainFontSize.value * 0.6, centerY + mainFontSize.value * 0.6)
|
||
ctx.fillText(text[3], centerX + mainFontSize.value * 0.6, centerY + mainFontSize.value * 0.6)
|
||
} else {
|
||
// 其他情况:单行显示
|
||
ctx.fillText(text, centerX, centerY)
|
||
}
|
||
}
|
||
|
||
// 绘制弧形文字
|
||
const drawCurvedText = (ctx: CanvasRenderingContext2D, text: string, centerX: number, centerY: number, radius: number, startAngle: number) => {
|
||
ctx.font = buildFontStyle(mainFontSize.value, mainFontWeight.value, mainFontItalic.value, mainFontFamily.value)
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'middle'
|
||
|
||
// 计算文字弧度范围,让文字均匀分布在上半圆(从左上到右上)
|
||
const baseArc = Math.PI * 0.75 // 基础弧度范围,约135度
|
||
const totalArc = baseArc * textSpacing.value // 根据间距调整总弧度
|
||
const angleStep = totalArc / (text.length - 1) // 文字间的角度间隔
|
||
const centerAngle = Math.PI * 1.5 // 上半圆中心位置(270度,Canvas中的顶部)
|
||
const startPos = centerAngle - totalArc / 2 // 从中心向左偏移一半弧度作为起始位置
|
||
|
||
// 椭圆形参数
|
||
const isOval = sealType.value === 'company_oval'
|
||
const radiusX = isOval ? radius : radius
|
||
const radiusY = isOval ? radius * 0.8 : radius // 椭圆形的Y轴半径是X轴的80%
|
||
|
||
for (let i = 0; i < text.length; i++) {
|
||
const angle = startPos + i * angleStep // 从左到右递增
|
||
|
||
// 根据印章类型计算文字位置
|
||
let x, y
|
||
if (isOval) {
|
||
// 椭圆形:使用椭圆方程计算位置
|
||
x = centerX + Math.cos(angle) * radiusX
|
||
y = centerY + Math.sin(angle) * radiusY
|
||
} else {
|
||
// 圆形:使用圆形方程计算位置
|
||
x = centerX + Math.cos(angle) * radius
|
||
y = centerY + Math.sin(angle) * radius
|
||
}
|
||
|
||
ctx.save()
|
||
ctx.translate(x, y)
|
||
// 椭圆形和圆形使用相同的文字旋转角度
|
||
ctx.rotate(angle + Math.PI / 2)
|
||
|
||
ctx.fillText(text[i], 0, 0)
|
||
ctx.restore()
|
||
}
|
||
}
|
||
|
||
// 绘制副文字弧形排列
|
||
const drawSubCurvedText = (ctx: CanvasRenderingContext2D, text: string, centerX: number, centerY: number, radius: number) => {
|
||
ctx.font = buildFontStyle(subFontSize.value, subFontWeight.value, subFontItalic.value, subFontFamily.value)
|
||
ctx.textAlign = 'center'
|
||
ctx.textBaseline = 'middle'
|
||
|
||
// 计算文字弧度范围,让文字均匀分布在下半圆,以底部中心为基准居中分布
|
||
const baseArc = Math.PI * 0.6 // 基础弧度范围,约108度
|
||
const totalArc = baseArc * subTextSpacing.value // 根据间距调整总弧度
|
||
const angleStep = totalArc / (text.length - 1) // 文字间的角度间隔
|
||
const centerAngle = Math.PI * 0.5 // 下半圆中心位置(90度,Canvas中的底部)
|
||
const startPos = centerAngle - totalArc / 2 // 从中心向左偏移一半弧度作为起始位置
|
||
|
||
// 椭圆形参数
|
||
const isOval = sealType.value === 'company_oval'
|
||
const radiusX = isOval ? radius : radius
|
||
const radiusY = isOval ? radius * 0.8 : radius // 椭圆形的Y轴半径是X轴的80%
|
||
|
||
for (let i = 0; i < text.length; i++) {
|
||
const angle = startPos + (text.length - 1 - i) * angleStep // 从右到左分布,修复顺序
|
||
|
||
// 根据印章类型计算文字位置
|
||
let x, y
|
||
if (isOval) {
|
||
// 椭圆形:使用椭圆方程计算位置
|
||
x = centerX + Math.cos(angle) * radiusX
|
||
y = centerY + Math.sin(angle) * radiusY
|
||
} else {
|
||
// 圆形:使用圆形方程计算位置
|
||
x = centerX + Math.cos(angle) * radius
|
||
y = centerY + Math.sin(angle) * radius
|
||
}
|
||
|
||
ctx.save()
|
||
ctx.translate(x, y)
|
||
// 修复文字旋转角度,下半圆的文字需要向内旋转
|
||
ctx.rotate(angle - Math.PI / 2)
|
||
|
||
ctx.fillText(text[i], 0, 0)
|
||
ctx.restore()
|
||
}
|
||
}
|
||
|
||
// 绘制五角星
|
||
const drawStar = (ctx: CanvasRenderingContext2D, centerX: number, centerY: number, radius: number) => {
|
||
const spikes = 5
|
||
const outerRadius = radius
|
||
const innerRadius = radius * 0.4
|
||
|
||
ctx.beginPath()
|
||
for (let i = 0; i < spikes * 2; i++) {
|
||
const angle = (i * Math.PI) / spikes - Math.PI / 2
|
||
const r = i % 2 === 0 ? outerRadius : innerRadius
|
||
const x = centerX + Math.cos(angle) * r
|
||
const y = centerY + Math.sin(angle) * r
|
||
|
||
if (i === 0) {
|
||
ctx.moveTo(x, y)
|
||
} else {
|
||
ctx.lineTo(x, y)
|
||
}
|
||
}
|
||
ctx.closePath()
|
||
ctx.fill()
|
||
}
|
||
|
||
// 绘制中心图形
|
||
const drawCenterShape = (ctx: CanvasRenderingContext2D, centerX: number, centerY: number, size: number, shape: string) => {
|
||
ctx.fillStyle = sealColor.value
|
||
|
||
switch (shape) {
|
||
case 'star':
|
||
drawStar(ctx, centerX, centerY, size)
|
||
break
|
||
|
||
case 'circle':
|
||
ctx.beginPath()
|
||
ctx.arc(centerX, centerY, size, 0, 2 * Math.PI)
|
||
ctx.fill()
|
||
break
|
||
|
||
case 'diamond':
|
||
ctx.beginPath()
|
||
ctx.moveTo(centerX, centerY - size)
|
||
ctx.lineTo(centerX + size, centerY)
|
||
ctx.lineTo(centerX, centerY + size)
|
||
ctx.lineTo(centerX - size, centerY)
|
||
ctx.closePath()
|
||
ctx.fill()
|
||
break
|
||
|
||
case 'hexagram':
|
||
// 绘制六角星(两个三角形重叠)
|
||
const hexSize = size * 0.8
|
||
ctx.beginPath()
|
||
// 第一个三角形(向上)
|
||
for (let i = 0; i < 3; i++) {
|
||
const angle = (i * 2 * Math.PI) / 3 - Math.PI / 2
|
||
const x = centerX + Math.cos(angle) * hexSize
|
||
const y = centerY + Math.sin(angle) * hexSize
|
||
if (i === 0) ctx.moveTo(x, y)
|
||
else ctx.lineTo(x, y)
|
||
}
|
||
ctx.closePath()
|
||
ctx.fill()
|
||
|
||
ctx.beginPath()
|
||
// 第二个三角形(向下)
|
||
for (let i = 0; i < 3; i++) {
|
||
const angle = (i * 2 * Math.PI) / 3 + Math.PI / 2
|
||
const x = centerX + Math.cos(angle) * hexSize
|
||
const y = centerY + Math.sin(angle) * hexSize
|
||
if (i === 0) ctx.moveTo(x, y)
|
||
else ctx.lineTo(x, y)
|
||
}
|
||
ctx.closePath()
|
||
ctx.fill()
|
||
break
|
||
|
||
case 'triangle':
|
||
ctx.beginPath()
|
||
ctx.moveTo(centerX, centerY - size)
|
||
ctx.lineTo(centerX + size * 0.866, centerY + size * 0.5)
|
||
ctx.lineTo(centerX - size * 0.866, centerY + size * 0.5)
|
||
ctx.closePath()
|
||
ctx.fill()
|
||
break
|
||
}
|
||
}
|
||
|
||
// 应用破损效果
|
||
const applyDamageEffect = (ctx: CanvasRenderingContext2D) => {
|
||
if (damageStyle.value === 'none' || wearLevel.value === 0) return
|
||
|
||
const width = canvasWidth.value
|
||
const height = canvasHeight.value
|
||
const intensity = wearLevel.value / 50 // 0-1.0 的强度,提高敏感度
|
||
|
||
// 使用destination-out合成模式,让绘制的内容从印章中"减去"
|
||
ctx.globalCompositeOperation = 'destination-out'
|
||
|
||
switch (damageStyle.value) {
|
||
case 'random':
|
||
applyRandomNoise(ctx, width, height, intensity)
|
||
break
|
||
case 'cracks':
|
||
applyCracks(ctx, width, height, intensity)
|
||
break
|
||
case 'edge':
|
||
applyEdgeWear(ctx, width, height, intensity)
|
||
break
|
||
case 'chunks':
|
||
applyChunks(ctx, width, height, intensity)
|
||
break
|
||
case 'stains':
|
||
applyStains(ctx, width, height, intensity)
|
||
break
|
||
}
|
||
|
||
// 恢复正常合成模式
|
||
ctx.globalCompositeOperation = 'source-over'
|
||
}
|
||
|
||
// 简单随机数生成器(基于文字内容,保证同样的文字产生相同的效果)
|
||
const getRandomGenerator = () => {
|
||
let seed = 12345
|
||
const text = mainText.value || 'default'
|
||
for (let i = 0; i < text.length; i++) {
|
||
seed += text.charCodeAt(i) * (i + 1)
|
||
}
|
||
seed += sealSize.value
|
||
|
||
return (min: number = 0, max: number = 1) => {
|
||
seed = (seed * 9301 + 49297) % 233280
|
||
const value = seed / 233280
|
||
return min + value * (max - min)
|
||
}
|
||
}
|
||
|
||
// 随机磨损效果
|
||
const applyRandomNoise = (ctx: CanvasRenderingContext2D, width: number, height: number, intensity: number) => {
|
||
const random = getRandomGenerator()
|
||
// 基础斑点数量 + 随强度增加的斑点
|
||
const baseSpots = 8 + Math.floor(intensity * 10) // 基础8-18个斑点
|
||
const additionalSpots = Math.floor(intensity * 50) + Math.floor(intensity * intensity * 80) // 大幅增加斑点数量
|
||
const totalSpots = baseSpots + additionalSpots
|
||
|
||
for (let i = 0; i < totalSpots; i++) {
|
||
const centerX = random(0, width)
|
||
const centerY = random(0, height)
|
||
|
||
// 斑点大小:基础大小 + 随强度增长
|
||
const baseSize = random(0.8, 3) // 基础随机大小
|
||
const growthFactor = i < baseSpots ? 1 : (1 + intensity * 3) // 后期增加的斑点更大
|
||
const size = baseSize * growthFactor + (intensity * 5) + (intensity * intensity * 8) // 增加大小增长幅度
|
||
|
||
// 随机透明度:基础透明度 + 随机变化
|
||
const baseAlpha = 0.5 + intensity * 0.4 // 提高基础透明度
|
||
const randomAlpha = baseAlpha + random(-0.15, 0.25) // 添加随机变化
|
||
const finalAlpha = Math.max(0.3, Math.min(0.95, randomAlpha)) // 提高透明度范围
|
||
|
||
// 创建白色径向渐变斑点(参考污渍效果)
|
||
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, size)
|
||
gradient.addColorStop(0, `rgba(255, 255, 255, ${finalAlpha})`)
|
||
gradient.addColorStop(0.6, `rgba(255, 255, 255, ${finalAlpha * 0.5})`)
|
||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
||
|
||
ctx.fillStyle = gradient
|
||
ctx.beginPath()
|
||
ctx.arc(centerX, centerY, size, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
}
|
||
}
|
||
|
||
// 裂纹效果
|
||
const applyCracks = (ctx: CanvasRenderingContext2D, width: number, height: number, intensity: number) => {
|
||
const random = getRandomGenerator()
|
||
const crackCount = Math.floor(intensity * 8) + 3 // 增加裂纹数量
|
||
|
||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)' // 白色半透明裂纹
|
||
ctx.lineCap = 'round'
|
||
|
||
for (let i = 0; i < crackCount; i++) {
|
||
const startX = random(width * 0.1, width * 0.9) // 扩大范围
|
||
const startY = random(height * 0.1, height * 0.9)
|
||
const angle = random(0, Math.PI * 2)
|
||
const length = random(30, 120) * intensity // 增加长度
|
||
const segments = Math.floor(length / 5)
|
||
|
||
// 裂纹宽度:线性增长 + 平方增长
|
||
const baseWidth = random(0.8, 1.5)
|
||
ctx.lineWidth = baseWidth + (intensity * 2) + (intensity * intensity * 4) // 更明显的增长
|
||
ctx.beginPath()
|
||
ctx.moveTo(startX, startY)
|
||
|
||
let currentX = startX
|
||
let currentY = startY
|
||
|
||
for (let j = 0; j < segments; j++) {
|
||
const deviation = (random(-1, 1) * 10) * intensity
|
||
const segmentAngle = angle + deviation * Math.PI / 180
|
||
const segmentLength = length / segments
|
||
|
||
currentX += Math.cos(segmentAngle) * segmentLength
|
||
currentY += Math.sin(segmentAngle) * segmentLength
|
||
|
||
ctx.lineTo(currentX, currentY)
|
||
}
|
||
|
||
ctx.stroke()
|
||
}
|
||
}
|
||
|
||
// 边缘磨损效果
|
||
const applyEdgeWear = (ctx: CanvasRenderingContext2D, width: number, height: number, intensity: number) => {
|
||
const random = getRandomGenerator()
|
||
const centerX = width / 2
|
||
const centerY = height / 2
|
||
const radius = Math.min(width, height) * 0.45
|
||
|
||
// 在边缘附近生成磨损点
|
||
const wearCount = Math.floor(radius * intensity * 0.8) + 30 // 增加磨损点数量
|
||
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)' // 白色半透明磨损点
|
||
|
||
for (let i = 0; i < wearCount; i++) {
|
||
const angle = random(0, Math.PI * 2)
|
||
const distance = radius + random(-20, 8) // 扩大磨损范围
|
||
const x = centerX + Math.cos(angle) * distance
|
||
const y = centerY + Math.sin(angle) * distance
|
||
// 磨损点大小:线性增长 + 平方增长
|
||
const baseSize = random(0.8, 1.8)
|
||
const size = baseSize + (intensity * 2.5) + (intensity * intensity * 3) // 更明显的增长
|
||
|
||
ctx.beginPath()
|
||
ctx.arc(x, y, size, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
}
|
||
}
|
||
|
||
// 块状缺失效果
|
||
const applyChunks = (ctx: CanvasRenderingContext2D, width: number, height: number, intensity: number) => {
|
||
const random = getRandomGenerator()
|
||
const chunkCount = Math.floor(intensity * 10) + 5 // 增加块状数量
|
||
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)' // 白色半透明块状
|
||
|
||
for (let i = 0; i < chunkCount; i++) {
|
||
const centerX = random(width * 0.05, width * 0.95) // 扩大范围
|
||
const centerY = random(height * 0.05, height * 0.95)
|
||
// 块状大小:线性增长 + 平方增长
|
||
const baseSize = random(2, 5)
|
||
const size = baseSize + (intensity * 8) + (intensity * intensity * 15) // 更明显的增长
|
||
|
||
// 绘制不规则块状
|
||
ctx.beginPath()
|
||
const points = 6 + Math.floor(random(0, 4))
|
||
|
||
for (let j = 0; j < points; j++) {
|
||
const angle = (j / points) * Math.PI * 2
|
||
const radius = size * (0.7 + random(0, 0.6))
|
||
const x = centerX + Math.cos(angle) * radius
|
||
const y = centerY + Math.sin(angle) * radius
|
||
|
||
if (j === 0) {
|
||
ctx.moveTo(x, y)
|
||
} else {
|
||
ctx.lineTo(x, y)
|
||
}
|
||
}
|
||
|
||
ctx.closePath()
|
||
ctx.fill()
|
||
}
|
||
}
|
||
|
||
// 污渍斑点效果
|
||
const applyStains = (ctx: CanvasRenderingContext2D, width: number, height: number, intensity: number) => {
|
||
const random = getRandomGenerator()
|
||
const stainCount = Math.floor(intensity * 20) + 8 // 增加污渍数量
|
||
|
||
for (let i = 0; i < stainCount; i++) {
|
||
const centerX = random(0, width)
|
||
const centerY = random(0, height)
|
||
// 污渍大小:线性增长 + 平方增长
|
||
const baseSize = random(1.5, 4)
|
||
const size = baseSize + (intensity * 6) + (intensity * intensity * 10) // 更明显的增长
|
||
|
||
// 创建白色径向渐变污渍
|
||
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, size)
|
||
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.7 + intensity * 0.3})`)
|
||
gradient.addColorStop(0.7, `rgba(255, 255, 255, ${0.3 + intensity * 0.2})`)
|
||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
||
|
||
ctx.fillStyle = gradient
|
||
ctx.beginPath()
|
||
ctx.arc(centerX, centerY, size, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
}
|
||
}
|
||
|
||
// 下载印章
|
||
const downloadSeal = async () => {
|
||
|
||
try {
|
||
// 创建临时画布用于导出
|
||
const tempCanvas = document.createElement('canvas')
|
||
tempCanvas.width = canvasWidth.value
|
||
tempCanvas.height = canvasHeight.value
|
||
const tempCtx = tempCanvas.getContext('2d')
|
||
if (!tempCtx) return
|
||
|
||
// 如果不是透明导出,先绘制背景
|
||
if (!exportTransparent.value) {
|
||
tempCtx.fillStyle = backgroundColor.value
|
||
tempCtx.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
|
||
}
|
||
|
||
// 绘制印章内容
|
||
tempCtx.strokeStyle = sealColor.value
|
||
tempCtx.fillStyle = sealColor.value
|
||
|
||
// 根据印章类型绘制
|
||
if (sealType.value.startsWith('company')) {
|
||
drawCompanySeal(tempCtx)
|
||
} else {
|
||
drawPersonalSeal(tempCtx)
|
||
}
|
||
|
||
// 应用破损效果
|
||
if (damageStyle.value !== 'none' && wearLevel.value > 0) {
|
||
applyDamageEffect(tempCtx)
|
||
}
|
||
|
||
// 生成下载链接
|
||
const link = document.createElement('a')
|
||
const backgroundSuffix = exportTransparent.value ? 'transparent' : 'background'
|
||
const damageSuffix = damageStyle.value !== 'none' && wearLevel.value > 0 ? `-${damageStyle.value}${wearLevel.value}` : ''
|
||
const namePrefix = mainText.value.trim() || centerText.value.trim() || 'custom'
|
||
link.download = `seal-${namePrefix}-${backgroundSuffix}${damageSuffix}-${Date.now()}.png`
|
||
link.href = tempCanvas.toDataURL('image/png')
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
} catch (error) {
|
||
console.error('下载失败:', error)
|
||
}
|
||
}
|
||
|
||
// 清空所有内容
|
||
const clearAll = () => {
|
||
mainText.value = ''
|
||
centerText.value = '印'
|
||
headerText.value = ''
|
||
subText.value = ''
|
||
personalHeight.value = 1.0
|
||
|
||
// 重置所有字体样式
|
||
mainFontWeight.value = false
|
||
mainFontItalic.value = false
|
||
mainFontFamily.value = 'SimSun'
|
||
|
||
centerFontWeight.value = false
|
||
centerFontItalic.value = false
|
||
centerFontFamily.value = 'SimSun'
|
||
|
||
headerFontWeight.value = false
|
||
headerFontItalic.value = false
|
||
headerFontFamily.value = 'SimSun'
|
||
|
||
subFontWeight.value = false
|
||
subFontItalic.value = false
|
||
subFontFamily.value = 'SimSun'
|
||
subTextCurved.value = false
|
||
subTextSpacing.value = 1.0
|
||
subTextMargin.value = 0
|
||
centerShape.value = 'star'
|
||
centerShapeSize.value = 15
|
||
exportTransparent.value = false
|
||
wearLevel.value = 0
|
||
damageStyle.value = 'none'
|
||
|
||
// 清空后重新绘制
|
||
drawSeal()
|
||
}
|
||
|
||
// 监听所有参数变化,实时重新绘制
|
||
watch([sealType, sealSize, canvasWidth, canvasHeight, personalHeight,
|
||
mainFontWeight, mainFontItalic, mainFontFamily,
|
||
centerFontWeight, centerFontItalic, centerFontFamily,
|
||
headerFontWeight, headerFontItalic, headerFontFamily,
|
||
subFontWeight, subFontItalic, subFontFamily, subTextCurved, subTextSpacing, subTextMargin,
|
||
sealColor, backgroundColor, mainText, centerText, headerText, subText,
|
||
mainFontSize, centerFontSize, headerFontSize, subFontSize,
|
||
outerBorderWidth, innerBorderWidth, showStar, centerShape, centerShapeSize, textSpacing, textMargin, wearLevel, damageStyle], () => {
|
||
drawSeal()
|
||
}, { deep: true })
|
||
|
||
// 组件挂载时绘制初始印章
|
||
onMounted(() => {
|
||
drawSeal()
|
||
|
||
// 延迟启用fixed定位,避免页面加载时的闪烁
|
||
setTimeout(() => {
|
||
isPreviewFixed.value = true
|
||
}, 100)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.slider {
|
||
@apply appearance-none bg-gray-200 dark:bg-gray-700 rounded-lg h-2 outline-none;
|
||
}
|
||
|
||
.slider::-webkit-slider-thumb {
|
||
@apply appearance-none w-4 h-4 rounded-full cursor-pointer;
|
||
background-color: rgb(99, 102, 241);
|
||
}
|
||
|
||
.slider::-moz-range-thumb {
|
||
@apply w-4 h-4 rounded-full cursor-pointer border-none;
|
||
background-color: rgb(99, 102, 241);
|
||
}
|
||
</style> |