add-relation.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <template>
  2. <view class="page-add-relation" :class="themeClass">
  3. <view class="form-card">
  4. <!-- Relation Type -->
  5. <view class="form-group">
  6. <text class="form-label">关系类型 *</text>
  7. <view class="type-grid">
  8. <view
  9. v-for="opt in relationTypeOptions"
  10. :key="opt.value"
  11. class="type-chip"
  12. :class="{ active: form.relationType === opt.value }"
  13. @click="form.relationType = opt.value"
  14. >
  15. <text class="chip-text">{{ opt.label }}</text>
  16. </view>
  17. </view>
  18. </view>
  19. <!-- Name -->
  20. <view class="form-group">
  21. <text class="form-label">姓名</text>
  22. <input
  23. v-model="form.name"
  24. class="form-input"
  25. placeholder="请输入姓名"
  26. placeholder-class="input-placeholder"
  27. maxlength="20"
  28. />
  29. </view>
  30. <!-- Birth Date -->
  31. <view class="form-group">
  32. <text class="form-label">生日 *</text>
  33. <picker mode="date" :value="form.birthDate" @change="onBirthDateChange">
  34. <view class="form-input picker-trigger">
  35. <text :class="{ 'text-placeholder': !form.birthDate }">
  36. {{ form.birthDate || '请选择出生日期' }}
  37. </text>
  38. </view>
  39. </picker>
  40. </view>
  41. <!-- Gender -->
  42. <view class="form-group">
  43. <text class="form-label">性别</text>
  44. <view class="gender-row">
  45. <view
  46. v-for="opt in genderOptions"
  47. :key="opt.value"
  48. class="gender-option"
  49. :class="{ active: form.gender === opt.value }"
  50. @click="form.gender = opt.value"
  51. >
  52. <text class="gender-text">{{ opt.label }}</text>
  53. </view>
  54. </view>
  55. </view>
  56. <!-- Note -->
  57. <view class="form-group">
  58. <text class="form-label">备注</text>
  59. <input
  60. v-model="form.note"
  61. class="form-input"
  62. placeholder="选填,如:工作中的合作伙伴"
  63. placeholder-class="input-placeholder"
  64. maxlength="50"
  65. />
  66. </view>
  67. </view>
  68. <!-- Save Button -->
  69. <view class="save-bar">
  70. <view class="btn-save" :class="{ saving: saving }" @click="handleSave">
  71. <text class="btn-save-text">{{ saving ? '保存中...' : '保存' }}</text>
  72. </view>
  73. </view>
  74. </view>
  75. </template>
  76. <script setup>
  77. import { ref, reactive } from 'vue'
  78. import { onLoad } from '@dcloudio/uni-app'
  79. import requestInstance from '@/utils/api'
  80. import { useTheme } from '@/composables/useThemeStyle'
  81. const { themeClass } = useTheme()
  82. const relationApi = {
  83. create: (data) => requestInstance.post('/relation/create', data),
  84. update: (data) => requestInstance.post('/relation/update', data),
  85. get: (id) => requestInstance.post('/relation/get', { id })
  86. }
  87. const relationTypeOptions = [
  88. { value: 'spouse', label: '夫妻' },
  89. { value: 'parent_child', label: '亲子' },
  90. { value: 'colleague', label: '同事' },
  91. { value: 'friend', label: '朋友' },
  92. { value: 'superior', label: '上下级' }
  93. ]
  94. const genderOptions = [
  95. { value: 0, label: '保密' },
  96. { value: 1, label: '男' },
  97. { value: 2, label: '女' }
  98. ]
  99. const form = reactive({
  100. relationType: '',
  101. name: '',
  102. birthDate: '',
  103. gender: 0,
  104. note: ''
  105. })
  106. const isEdit = ref(false)
  107. const editId = ref(null)
  108. const saving = ref(false)
  109. onLoad((query) => {
  110. if (query && query.id) {
  111. isEdit.value = true
  112. editId.value = query.id
  113. uni.setNavigationBarTitle({ title: '编辑关系人' })
  114. fetchRelation(query.id)
  115. }
  116. })
  117. async function fetchRelation(id) {
  118. try {
  119. const data = await relationApi.get(id)
  120. if (data) {
  121. form.relationType = data.relationType || ''
  122. form.name = data.name || ''
  123. form.birthDate = data.birthDate || ''
  124. form.gender = data.gender ?? 0
  125. form.note = data.note || ''
  126. }
  127. } catch (e) {
  128. uni.showToast({ title: '加载关系人信息失败', icon: 'none' })
  129. }
  130. }
  131. function onBirthDateChange(e) {
  132. form.birthDate = e.detail.value
  133. }
  134. function validate() {
  135. if (!form.relationType) {
  136. uni.showToast({ title: '请选择关系类型', icon: 'none' })
  137. return false
  138. }
  139. if (!form.birthDate) {
  140. uni.showToast({ title: '请选择出生日期', icon: 'none' })
  141. return false
  142. }
  143. return true
  144. }
  145. async function handleSave() {
  146. if (!validate()) return
  147. if (saving.value) return
  148. saving.value = true
  149. try {
  150. if (isEdit.value) {
  151. await relationApi.update({ id: editId.value, ...form })
  152. uni.showToast({ title: '更新成功', icon: 'success' })
  153. } else {
  154. await relationApi.create(form)
  155. uni.showToast({ title: '添加成功', icon: 'success' })
  156. }
  157. setTimeout(() => {
  158. uni.navigateBack()
  159. }, 1200)
  160. } catch (e) {
  161. // error toast already handled by requestInstance
  162. } finally {
  163. saving.value = false
  164. }
  165. }
  166. </script>
  167. <style scoped lang="scss">
  168. .page-add-relation {
  169. min-height: 100vh;
  170. background: var(--color-bg);
  171. padding: 16px;
  172. }
  173. /* Form card */
  174. .form-card {
  175. background: var(--color-bg-card);
  176. border-radius: 16px;
  177. padding: 24px 20px;
  178. box-shadow: var(--shadow-card);
  179. }
  180. .form-group {
  181. margin-bottom: 24px;
  182. }
  183. .form-group:last-child {
  184. margin-bottom: 0;
  185. }
  186. .form-label {
  187. display: block;
  188. font-size: 14px;
  189. font-weight: 600;
  190. color: var(--color-text-secondary);
  191. margin-bottom: 10px;
  192. }
  193. /* Input fields */
  194. .form-input {
  195. width: 100%;
  196. height: 44px;
  197. background: var(--color-bg-card);
  198. border: 1px solid var(--color-border);
  199. border-radius: 10px;
  200. padding: 0 14px;
  201. font-size: 15px;
  202. color: var(--color-text-primary);
  203. box-sizing: border-box;
  204. }
  205. .form-input:focus {
  206. border-color: var(--color-brand);
  207. box-shadow: 0 0 0 2px rgba(30, 58, 95, 0.1);
  208. }
  209. .input-placeholder {
  210. color: var(--color-text-secondary);
  211. font-size: 15px;
  212. }
  213. /* Picker */
  214. .picker-trigger {
  215. display: flex;
  216. align-items: center;
  217. }
  218. .text-placeholder {
  219. color: var(--color-text-secondary);
  220. font-size: 15px;
  221. }
  222. /* Relation type chips */
  223. .type-grid {
  224. display: flex;
  225. flex-wrap: wrap;
  226. gap: 10px;
  227. }
  228. .type-chip {
  229. padding: 10px 20px;
  230. border: 1px solid var(--color-border);
  231. border-radius: 10px;
  232. background: var(--color-bg-card);
  233. text-align: center;
  234. flex: 0 0 auto;
  235. min-width: 80px;
  236. }
  237. .type-chip:active {
  238. transform: scale(0.97);
  239. }
  240. .type-chip.active {
  241. border-color: var(--color-brand);
  242. background: var(--color-brand);
  243. }
  244. .type-chip.active .chip-text {
  245. color: #FFFFFF;
  246. }
  247. .chip-text {
  248. font-size: 14px;
  249. font-weight: 500;
  250. color: var(--color-text-secondary);
  251. }
  252. /* Gender row */
  253. .gender-row {
  254. display: flex;
  255. gap: 12px;
  256. }
  257. .gender-option {
  258. flex: 1;
  259. padding: 10px 0;
  260. border: 1px solid var(--color-border);
  261. border-radius: 10px;
  262. text-align: center;
  263. background: var(--color-bg-card);
  264. }
  265. .gender-option:active {
  266. transform: scale(0.97);
  267. }
  268. .gender-option.active {
  269. border-color: var(--color-brand);
  270. background: var(--color-brand);
  271. }
  272. .gender-option.active .gender-text {
  273. color: #FFFFFF;
  274. }
  275. .gender-text {
  276. font-size: 14px;
  277. font-weight: 500;
  278. color: var(--color-text-secondary);
  279. }
  280. /* Save button */
  281. .save-bar {
  282. margin-top: 24px;
  283. padding-bottom: 16px;
  284. }
  285. .btn-save {
  286. width: 100%;
  287. height: 48px;
  288. background: var(--color-brand);
  289. border-radius: 12px;
  290. display: flex;
  291. align-items: center;
  292. justify-content: center;
  293. }
  294. .btn-save:active {
  295. transform: scale(0.97);
  296. opacity: 0.9;
  297. }
  298. .btn-save.saving {
  299. opacity: 0.7;
  300. pointer-events: none;
  301. }
  302. .btn-save-text {
  303. color: #FFFFFF;
  304. font-size: 16px;
  305. font-weight: 600;
  306. }
  307. </style>